#!/usr/bin/env ruby

# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is fire-test-runner.
#
# The Initial Developer of the Original Code is Kouhei Sutou.
# Portions created by the Initial Developer are Copyright (C) 2010-2012
# the Initial Developer. All Rights Reserved.
#
# Contributor(s): Kouhei Sutou <kou@clear-code.com>
#                 mooz <stillpedant@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****


require 'socket'
require 'optparse'
require 'ostruct'
require 'pathname'

options = OpenStruct.new
options.host = "localhost"
options.port = 4444
options.firefox = "firefox"
options.named_profile = nil
options.profile = nil
options.wait = 3
options.n_retries = 3
options.max_parallel_count = -1
options.quit = false
options.close_main_windows = false
options.use_color = nil

parser = OptionParser.new("Usage: #{$0} [options] test_or_directory...")
parser.on("--host=HOST",
          "host name to connect Firefox (#{options.host})") do |host|
  options.host = host
end
parser.on("-pPORT", "--port=PORT", Integer,
          "port number to connect Firefox (#{options.port})") do |port|
  options.port = port
end
parser.on("--firefox=FIREFOX",
          "command to start Firefox (#{options.firefox})")  do |firefox|
  options.firefox = firefox
end
parser.on("--named-profile=PROFILE",
          "name of Firefox's profile",
          "(#{options.named_profile})") do |named_profile|
  options.named_profile = named_profile
end
parser.on("--profile=PROFILE",
          "path to Firefox's profile (#{options.profile})") do |profile|
  options.profile = profile
end
parser.on("--wait=SECONDS", Float,
          "how long wait to start Firefox (#{options.wait})") do |wait|
  options.wait = wait
end
parser.on("--max-parallel-count=N", Integer,
          "how many testcases to process tmem parallelly",
          "(#{options.max_parallel_count})") do |max_parallel_count|
  options.max_parallel_count = max_parallel_count
end
parser.on("--retries=N", Integer,
          "how many times to retry to connect",
          "(#{options.n_retries})") do |n_retries|
  options.n_retries = n_retries
end
parser.on("--[no-]run-all",
          "whether run all tests or only tests with high priority",
          "(#{options.run_all})") do |run_all|
  options.run_all = run_all
end
parser.on("--[no-]quit",
          "whether quit or not on finish (#{options.quit})") do |quit|
  options.quit = quit
end
parser.on("--[no-]close-main-windows",
          "whether close main windows or not",
          "(#{options.close_main_windows})") do |close_main_windows|
  options.close_main_windows = close_main_windows
end
parser.on("--[no-]use-color",
          "whether use colorized output or not", "(auto)") do |use_color|
  options.use_color = use_color
end

parser.separator ""
parser.on_tail("--help", "Show this message") do
  puts parser
  exit(true)
end

tests = parser.parse!(ARGV)
if tests.empty?
  $stderr.puts parser
  exit(false)
end

module RubyToJS
  def hash_to_jsobject(hash)
    hash_content_string = hash.map do |name, value|
      "#{name}: #{value}"
    end.join(", ")

    "{#{hash_content_string}}"
  end
end

class Runner
  include RubyToJS

  def initialize(socket, target_paths, output=$stdout)
    @socket = socket
    @output = output
    @output.sync = true
    @target_paths = target_paths
    @reporter_var_name = "reporter"
    @shown_result_index = 0
    self.use_color = nil
  end

  def use_color?
    @use_color
  end

  def use_color=(use_color)
    @use_color = use_color
    @use_color = guess_use_color if @use_color.nil?
  end

  def run(options)
    escaped_target_paths = @target_paths.collect do |path|
      normalized_path = normalize_separator(path)
      escaped_path = escape_path(normalized_path.to_s)
      "'#{escaped_path}'"
    end.join(", ")

    reporter_options = {}
    reporter_options["useColor"] = use_color?
    reporter_options["priority"] = "'must'" if options.run_all
    if options.max_parallel_count > -1
      reporter_options["maxParallelCount"] = options.max_parallel_count
    end
    reporter_options_string = hash_to_jsobject(reporter_options)

    evaluate("var #{@reporter_var_name};")
    evaluate("#{@reporter_var_name} =" +
             " runTest(#{reporter_options_string}, #{escaped_target_paths}); " +
             "undefined;")

    begin
      show_result while running?
    rescue Interrupt
      puts "Aborting..."
      abort
    end

    show_result
    @output.puts

    evaluate("#{@reporter_var_name}.wasSuccessful()", false)
    result = read.chomp
    !aborted? and result == "true"
  end

  def close_main_windows
    evaluate("closeMainWindows();")
  end

  def quit
    evaluate("quitApplication();")
  end

  def abort
    evaluate("#{@reporter_var_name}.abort()") unless aborted?
    @aborted = true
  end

  def aborted?
    @aborted
  end

  private
  def normalize_separator(path)
    if File::ALT_SEPARATOR
      normalized_file_name = Pathname(path.to_s.gsub(/\//, File::ALT_SEPARATOR))
    else
      normalized_file_name = path
    end
  end

  def escape_path(path)
    path.gsub(/\\/, "\\\\\\").gsub(/'/, "\\'")
  end

  def evaluate(command, need_to_read=true)
    return nil if @socket.nil?
    begin
      @socket.puts(command)
    rescue Errno::EPIPE
      @output.puts("#{$!.inspect}: #{$!}: #{command}")
      @output.puts($@)
      return nil
    end
    return nil unless need_to_read

    result = read.chomp
    @output.puts(result) unless result.empty?
    result
  end

  def read
    buffer = ""
    while IO.select([@socket], [], [], 0.1)
      break if @socket.eof?
      buffer << @socket.readpartial(4096)
    end
    buffer
  end

  def running?
    evaluate("#{@reporter_var_name}.isFinished()", false)
    result = nil
    loop do
      result = read.chomp
      break unless result.empty?
      return false if @socket.eof?
    end

    unless ["true", "false"].include?(result)
      @output.puts("Failed:")
      @output.puts(result)
      return false
    end
    result != "true"
  end

  def show_result
    evaluate("#{@reporter_var_name}.result", false)
    outputted = false
    n_tried = 4
    loop do
      result = read.chomp
      if result.empty? and !outputted
        if n_tried > 0
          sleep 0.5
          n_tried -= 1
          next
        end
      end
      break if result.size <= @shown_result_index
      @output.print(result[@shown_result_index..-1])
      @output.flush
      @shown_result_index = result.size
      outputted = true
    end
  end

  def guess_use_color
    return false unless @output.tty?
    term = ENV["TERM"]
    return true if term and (/term\z/ =~ term or term == "screen")
    return true if ENV["EMACS"] == "t"
    false
  end
end

def run(tests, options)
  succeeded = true
  TCPSocket.open(options.host, options.port) do |socket|
    begin
      runner = Runner.new(socket,
                          tests.collect {|test| Pathname(test).expand_path})
      runner.close_main_windows if options.close_main_windows
      succeeded = runner.run(options)
      runner.quit if options.quit
    rescue SystemCallError
      puts "#{$!.class}: #{$!}"
      puts $@
      succeeded = false
    end
  end
  succeeded
end

n_retried = 0
succeeded = false
begin
  succeeded = run(tests, options)
rescue SystemCallError
  n_retried += 1
  if n_retried <= options.n_retries
    command = [options.firefox,
               "-uxu-start-server",
               "-uxu-listen-port", options.port.to_s]
    command += ["-p", options.named_profile] if options.named_profile
    command += ["-profile", options.profile] if options.profile
    command += ["-no-remote"] if options.named_profile or options.profile
    if options.max_parallel_count > -1
      command += ["-uxu-max-parallel-count", options.max_parallel_count]
    end
    command += ["&"]
    system(command.join(" "))
    sleep options.wait
    retry
  end
  succeeded = false
end
exit(succeeded)
